]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
ci: Split POTFILES check between C and JS
authorFlorian Müllner <fmuellner@gnome.org>
Mon, 9 Aug 2021 02:00:43 +0000 (04:00 +0200)
committerMarge Bot <marge-bot@gnome.org>
Sat, 14 Aug 2021 18:45:25 +0000 (18:45 +0000)
Regex are a crude tool for analyzing whether some code *calls* a
particular function. Spidermonkey has Reflect.parse() that returns
the AST of the passed in code, which allows for a much more precise
check for javascript.

The old script is still used for C code, where i18n-affecting changes
are much rarer.

Based heavily on Philip Chimento's mozjs migration script at
https://gitlab.gnome.org/ptomato/moz60tool.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1941>

.gitlab-ci.yml
.gitlab-ci/check-potfiles.js [new file with mode: 0644]
.gitlab-ci/check-potfiles.sh

index 8ed0255956a3ce3d0ef18574e33c3531b492f4b0..e8c08cc1b7e475742f8ceec6a7070b1865ee6aaa 100644 (file)
@@ -160,7 +160,7 @@ eslint_mr:
             junit: ${LINT_MR_LOG}
         when: always
 
-potfile_check:
+potfile_c_check:
     extends:
         - .fdo.distribution-image@fedora
         - .gnome-shell.fedora:34
@@ -168,6 +168,14 @@ potfile_check:
     script:
         - ./.gitlab-ci/check-potfiles.sh
 
+potfile_js_check:
+    extends:
+        - .fdo.distribution-image@fedora
+        - .gnome-shell.fedora:34
+    stage: review
+    script:
+        - js78 -m .gitlab-ci/check-potfiles.js
+
 no_template_check:
     extends:
         - .fdo.distribution-image@fedora
diff --git a/.gitlab-ci/check-potfiles.js b/.gitlab-ci/check-potfiles.js
new file mode 100644 (file)
index 0000000..e89c5ad
--- /dev/null
@@ -0,0 +1,202 @@
+const gettextFuncs = new Set([
+    '_',
+    'N_',
+    'C_',
+    'NC_',
+    'dcgettext',
+    'dgettext',
+    'dngettext',
+    'dpgettext',
+    'gettext',
+    'ngettext',
+    'pgettext',
+]);
+
+function dirname(file) {
+    const split = file.split('/');
+    split.pop();
+    return split.join('/');
+}
+
+const scriptDir = dirname(import.meta.url);
+const root = dirname(scriptDir);
+
+const excludedFiles = new Set();
+const foundFiles = new Set()
+
+function addExcludes(filename) {
+    const contents = os.file.readFile(filename);
+    const lines = contents.split('\n')
+        .filter(l => l && !l.startsWith('#'));
+   lines.forEach(line => excludedFiles.add(line));
+}
+
+addExcludes(`${root}/po/POTFILES.in`);
+addExcludes(`${root}/po/POTFILES.skip`);
+
+function walkAst(node, func) {
+    func(node);
+    nodesToWalk(node).forEach(n => walkAst(n, func));
+}
+
+function findGettextCalls(node) {
+    switch(node.type) {
+    case 'CallExpression':
+        if (node.callee.type === 'Identifier' &&
+            gettextFuncs.has(node.callee.name))
+            throw new Error();
+        if (node.callee.type === 'MemberExpression' &&
+            node.callee.object.type === 'Identifier' &&
+            node.callee.object.name === 'Gettext' &&
+            node.callee.property.type === 'Identifier' &&
+            gettextFuncs.has(node.callee.property.name))
+            throw new Error();
+        break;
+    }
+    return true;
+}
+
+function nodesToWalk(node) {
+    switch(node.type) {
+    case 'ArrayPattern':
+    case 'BreakStatement':
+    case 'CallSiteObject':  // i.e. strings passed to template
+    case 'ContinueStatement':
+    case 'DebuggerStatement':
+    case 'EmptyStatement':
+    case 'Identifier':
+    case 'Literal':
+    case 'MetaProperty':  // i.e. new.target
+    case 'Super':
+    case 'ThisExpression':
+        return [];
+    case 'ArrowFunctionExpression':
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+        return [...node.defaults, node.body].filter(n => !!n);
+    case 'AssignmentExpression':
+    case 'BinaryExpression':
+    case 'ComprehensionBlock':
+    case 'LogicalExpression':
+        return [node.left, node.right];
+    case 'ArrayExpression':
+    case 'TemplateLiteral':
+        return node.elements.filter(n => !!n);
+    case 'BlockStatement':
+    case 'Program':
+        return node.body;
+    case 'CallExpression':
+    case 'NewExpression':
+    case 'TaggedTemplate':
+        return [node.callee, ...node.arguments];
+    case 'CatchClause':
+        return [node.body, node.guard].filter(n => !!n);
+    case 'ClassExpression':
+    case 'ClassStatement':
+        return [...node.body, node.superClass].filter(n => !!n);
+    case 'ClassMethod':
+        return [node.name, node.body];
+    case 'ComprehensionExpression':
+    case 'GeneratorExpression':
+        return [node.body, ...node.blocks, node.filter].filter(n => !!n);
+    case 'ComprehensionIf':
+        return [node.test];
+    case 'ComputedName':
+        return [node.name];
+    case 'ConditionalExpression':
+    case 'IfStatement':
+        return [node.test, node.consequent, node.alternate].filter(n => !!n);
+    case 'DoWhileStatement':
+    case 'WhileStatement':
+        return [node.body, node.test];
+    case 'ExportDeclaration':
+        return [node.declaration, node.source].filter(n => !!n);
+    case 'ImportDeclaration':
+        return [...node.specifiers, node.source];
+    case 'LetStatement':
+        return [...node.head, node.body];
+    case 'ExpressionStatement':
+        return [node.expression];
+    case 'ForInStatement':
+    case 'ForOfStatement':
+        return [node.body, node.left, node.right];
+    case 'ForStatement':
+        return [node.init, node.test, node.update, node.body].filter(n => !!n);
+    case 'LabeledStatement':
+        return [node.body];
+    case 'MemberExpression':
+        return [node.object, node.property];
+    case 'ObjectExpression':
+    case 'ObjectPattern':
+        return node.properties;
+    case 'OptionalExpression':
+        return [node.expression];
+    case 'OptionalMemberExpression':
+        return [node.object, node.property];
+    case 'Property':
+    case 'PrototypeMutation':
+        return [node.value];
+    case 'ReturnStatement':
+    case 'ThrowStatement':
+    case 'UnaryExpression':
+    case 'UpdateExpression':
+    case 'YieldExpression':
+        return node.argument ? [node.argument] : [];
+    case 'SequenceExpression':
+        return node.expressions;
+    case 'SpreadExpression':
+        return [node.expression];
+    case 'SwitchCase':
+        return [node.test, ...node.consequent].filter(n => !!n);
+    case 'SwitchStatement':
+        return [node.discriminant, ...node.cases];
+    case 'TryStatement':
+        return [node.block, node.handler, node.finalizer].filter(n => !!n);
+    case 'VariableDeclaration':
+        return node.declarations;
+    case 'VariableDeclarator':
+        return node.init ? [node.init] : [];
+    case 'WithStatement':
+        return [node.object, node.body];
+    default:
+        print(`Ignoring ${node.type}, you should probably fix this in the script`);
+    }
+}
+
+function walkDir(dir) {
+    os.file.listDir(dir).forEach(child => {
+        if (child.startsWith('.'))
+            return;
+
+        const path = os.path.join(dir, child);
+        const relativePath = path.replace(`${root}/`, '');
+        if (excludedFiles.has(relativePath))
+            return;
+
+        if (!child.endsWith('.js')) {
+            try {
+                walkDir(path);
+            } catch (e) {
+                // not a directory
+            }
+            return;
+        }
+
+        try {
+            const script = os.file.readFile(path);
+            const ast = Reflect.parse(script);
+            walkAst(ast, findGettextCalls);
+        } catch (e) {
+            foundFiles.add(path);
+        }
+    });
+}
+
+walkDir(root);
+
+if (foundFiles.size === 0)
+    quit(0);
+
+print('The following files are missing from po/POTFILES.in:')
+foundFiles.forEach(f => print(`  ${f}`));
+quit(1);
index 8785eb8077e7ef29fd47d1ac2941b2a6f07fa0c0..a36fe7539b9515bf56f2f1551aaf1c1235812564 100755 (executable)
@@ -1,10 +1,9 @@
 #!/usr/bin/env bash
 
-srcdirs="js src subprojects/extensions-tool"
-globs=('*.js' '*.c')
+srcdirs="src subprojects/extensions-tool"
 
 # find source files that contain gettext keywords
-files=$(grep -lR ${globs[@]/#/--include=} '\(gettext\|[^I_)]_\)(' $srcdirs)
+files=$(grep -lR --include='*.c' '\(gettext\|[^I_)]_\)(' $srcdirs)
 
 # filter out excluded files
 if [ -f po/POTFILES.skip ]; then